cmake_minimum_required(VERSION 3.20)
project(ngscopeclient VERSION 0.1)
include_directories(cmake) #WORKAROUND needed for find_package(Vulkan) for Vulkan SDK
enable_language(CXX)

# Warn if platform is not 64-bit
if(NOT (CMAKE_SIZEOF_VOID_P EQUAL 8))
	message(WARNING "You are attempting to build on a 32-bit platform. While this may work and we may accept fixes "
		              "upstream, it will most likely fail and is unsupported.")
endif()

# Check for Vulkan SDK env var and prepend it to CMAKE_PREFIX_PATH if set to prefer Vulkan SDK packages if available
if(DEFINED ENV{VULKAN_SDK})
	#WORKAROUND: Vulkan SDK is incompatible with MinGW due to C++ linkage
	if(WIN32 AND (NOT CMAKE_GENERATOR MATCHES "Visual Studio"))
		message(WARNING "Detected VULKAN_SDK at $ENV{VULKAN_SDK}.")
		message(WARNING "Compiling and linking against the Vulkan SDK is not supported with MSYS. The build system "
			      "will ignore the SDK and you will need to have the Vulkan packages installed from the MSYS repositories.")
	else()
		message("Detected and using VULKAN_SDK at $ENV{VULKAN_SDK}")
		cmake_path(CONVERT "$ENV{VULKAN_SDK}" TO_CMAKE_PATH_LIST VULKAN_SDK_PATH NORMALIZE)
		list(PREPEND CMAKE_PREFIX_PATH ${VULKAN_SDK_PATH})
	endif()
endif()

# Git is used for git-describe based version generation if we have it
find_package(Git)

#Set up versioning (with a dummy string for now if Git isn't present)
if(Git_FOUND)
	execute_process(
		COMMAND ${GIT_EXECUTABLE} describe --always --tags
		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
		OUTPUT_VARIABLE NGSCOPECLIENT_VERSION
		OUTPUT_STRIP_TRAILING_WHITESPACE)
	message("Git reports scopehal-apps version ${NGSCOPECLIENT_VERSION}")


	execute_process(
		COMMAND ${GIT_EXECUTABLE} describe --always --tags --long
		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
		OUTPUT_VARIABLE NGSCOPECLIENT_VERSION_LONG
		OUTPUT_STRIP_TRAILING_WHITESPACE)

	# TODO: if/when we have a point release, make MSI version 10x+9
	# ex: 0.1.2-rc2 is 0.1.22
	# ex: 0.1.2 is 0.1.29

	# Ugly string parsing to make windows build happy
	# First path: release candidate tags
	if(NGSCOPECLIENT_VERSION_LONG MATCHES "v([0-9]*).([0-9]*)-rc([0-9])-([0-9]*)")
		set(MSI_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}.${CMAKE_MATCH_4}")

	# Release tags
	elseif(NGSCOPECLIENT_VERSION_LONG MATCHES "v([0-9]*).([0-9]*)-([0-9]*)")
		set(MSI_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.9.${CMAKE_MATCH_3}")
	endif()

else()
	set(NGSCOPECLIENT_VERSION "unknown")
	set(MSI_VERSION "0.1")
	message("Git not detected, scopehal-apps version unknown")
endif()

set(PROJECT_VERSION "${NGSCOPECLIENT_VERSION}")

include(CTest)

# Configuration settings
set(BUILD_DOCS CACHE BOOL "Build end user documentation")
set(BUILD_DEVDOCS CACHE BOOL "Build developer documentation")
set(ANALYZE CACHE BOOL "Run static analysis on the code, requires cppcheck and clang-analyzer to be installed")
set(DISABLE_PCH CACHE BOOL "Disable precompiled headers as this may break certain configurations")
set(BUILD_TESTING CACHE BOOL "Build unit tests")

# Build with C++ 17 on Linux for maximum compatibility with older system
# but for Windows, enable C++ 20 to bypass a deprecation warning
# (workaround for https://github.com/KhronosGroup/Vulkan-Hpp/issues/2034)
# even though we do not yet use any C++ 20 features in our codebase
if(WIN32)
	set(CMAKE_CXX_STANDARD 20)
else()
	set(CMAKE_CXX_STANDARD 17)
endif()

set(CMAKE_CXX_STANDARD_REQUIRED ON) # error if compiler doesn't support c++17
set(CMAKE_CXX_EXTENSIONS OFF) # use c++17 instead of gnu++17

# PCH needs to be disabled before targets are created/added/PCH is added to them
if(DISABLE_PCH)
	set(CMAKE_DISABLE_PRECOMPILE_HEADERS ON)
endif()

# Compiler warnings for GCC/Clang
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
	add_compile_options(-Wall -Wextra -Wuninitialized)
	add_compile_options(-Wshadow -Wpedantic -Wcast-align -Wwrite-strings)
	add_compile_options(-Wmissing-declarations -Wvla)
  add_compile_options(-Werror -Wno-error=deprecated-declarations -Wno-error=unused-parameter -Wno-error=unused-variable -Wno-error=unused-result -Wno-error=shadow)
endif()

# Compiler warnings specific to GCC or Clang
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 13)
	add_compile_options(-Woverloaded-virtual=1)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Woverloaded-virtual -Wpointer-sign -Wno-gnu-zero-variadic-macro-arguments -Wno-unknown-warning-option)
endif()
add_compile_options(-mtune=native)
add_compile_options("$<$<CONFIG:RELEASE>:-O3>")
add_compile_options("$<$<CONFIG:RELWITHDEBINFO>:-O3;-g>")
add_compile_options("$<$<CONFIG:DEBUG>:-Og;-D_DEBUG;-g>")
add_compile_options("$<$<CONFIG:DEBUGNOOPT>:-O0;-D_DEBUG;-g>")

# Needed for %zu/%zd on MSYS2, must be added here to avoid redefinition
if(WIN32 AND (NOT MSVC))
    add_compile_definitions(__USE_MINGW_ANSI_STDIO=1)
endif()

# Default build type
if(NOT CMAKE_BUILD_TYPE)
	set(CMAKE_BUILD_TYPE "Release")
endif()

if(SANITIZE)
	add_compile_options(-fsanitize=address -fsanitize=undefined)
	add_link_options(-fsanitize=address -fsanitize=undefined)
endif()

if(WIN32)
	add_compile_options(-D_USE_MATH_DEFINES -D_POSIX_THREAD_SAFE_FUNCTIONS)
endif()

# Package detection
find_package(PkgConfig MODULE REQUIRED)

# yaml-cpp is used in scopehal and ngscopeclient
find_package(yaml-cpp REQUIRED)
#WORKAROUND Needed for Debian Bullseye, which does not provide a yaml-cpp::yaml-cpp target
if(NOT TARGET yaml-cpp::yaml-cpp)
	find_library(YAML_CPP_LIBRARIES_FILES NAMES ${YAML_CPP_LIBRARIES})
	if(YAML_CPP_LIBRARIES_FILES MATCHES ".so$")
		add_library(yaml-cpp::yaml-cpp SHARED IMPORTED)
	elseif(YAML_CPP_LIBRARIES_FILES MATCHES ".a$")
		add_library(yaml-cpp::yaml-cpp STATIC IMPORTED)
	else()
		message(FATAL_ERROR "Unexpected partially-installed yaml-cpp, is it installed correctly?")
	endif()
	set_property(TARGET yaml-cpp::yaml-cpp PROPERTY IMPORTED_LOCATION ${YAML_CPP_LIBRARIES_FILES})
	#WORKAROUND The cmake file for yaml-cpp on debian bullseye is broken and provides a wrong YAML_CPP_INCLUDE_DIR
	find_path(YAML_CPP_INCLUDEFILES_DIR yaml-cpp/yaml.h REQUIRED)
	cmake_path(GET YAML_CPP_INCLUDEFILES_DIR PARENT_PATH YAML_CPP_INCLUDE_DIR)
	set_property(TARGET yaml-cpp::yaml-cpp PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${YAML_CPP_INCLUDEFILES_DIR})
endif()

pkg_check_modules(SIGCXX QUIET IMPORTED_TARGET sigc++-3.0) # look for latest version first
if(NOT SIGCXX_FOUND)
	pkg_check_modules(SIGCXX QUIET IMPORTED_TARGET sigc++-2.0) # look for older version too
endif()
if(NOT SIGCXX_FOUND)
	message(FATAL_ERROR "Unable to find any version of sigc++; this is required to build ngscopeclient.")
endif()

# We still use gtk on Linux for the file browser dialog in "native" mode)
if(LINUX)
	pkg_check_modules(GTK QUIET IMPORTED_TARGET REQUIRED gtk+-3.0)
	pkg_search_module(WAYLAND_CLIENT wayland-client IMPORTED_TARGET GLOBAL REQUIRED)
endif()

#WORKAROUND Fedora 38 does not include Threads from the glslang .cmake file, fixed in Fedora 39+
find_package(Threads MODULE REQUIRED)

# Configure and enable OpenMP
find_package(OpenMP MODULE)
if(NOT OpenMP_CXX_FOUND)
	message(FATAL_ERROR "ngscopeclient requires OpenMP but your C++ compiler does not appear to support it.")
endif()

# GLFW is required
find_package(glfw3 3.2 REQUIRED)

# Find Vulkan-related packages
#WORKAROUND glslang does not look for SPIRV-Tools-opt but needs it on macOS, harmless elsewhere
find_package(SPIRV-Tools-opt REQUIRED)
if(DEFINED VULKAN_SDK_PATH)
	#WORKAROUND The Vulkan SDK ships incomplete cmake config files.
	# Work around this, which does require FindVulkan.cmake.
	# FindVulkan does not find glslang correctly on macOS
	find_package(Vulkan REQUIRED COMPONENTS glslc shaderc_combined SPIRV-Tools)
	find_package(glslang REQUIRED)
	add_library(Vulkan::Loader ALIAS Vulkan::Vulkan)
else()
	find_package(glslang REQUIRED)
	find_package(VulkanHeaders REQUIRED)
	find_package(VulkanLoader QUIET)
	if(NOT VulkanLoader_FOUND) #WORKAROUND Alpine Linux has missing cmake files for VulkanLoader
		find_library(VulkanLoader_LIB libvulkan.so REQUIRED)
		cmake_path(GET VulkanLoader_LIB FILENAME VulkanLoader_LIB_NAME)
		add_library(Vulkan::Loader SHARED IMPORTED)
		set_property(TARGET Vulkan::Loader PROPERTY IMPORTED_LOCATION ${VulkanLoader_LIB})
	endif()
endif()
# The following should work regardless of Vulkan SDK or individual components
# shaderc does not provide a .cmake file, but does provide a pkgconfig file
# pkgconfig file is not provided by Vulkan SDK, however
pkg_check_modules(SHADERC shaderc QUIET IMPORTED_TARGET)
# This is needed due to shaderc not providing a programmatic way to derive the path of the glslc executable
if(SHADERC_FOUND)
	cmake_path(GET SHADERC_INCLUDE_DIRS PARENT_PATH SHADERC_PREFIX)
endif()
# This should find glslc from either shaderc or the Vulkan SDK
find_program(Vulkan_GLSLC_EXECUTABLE glslc HINTS SHADERC_PREFIX)

if(NOT Vulkan_GLSLC_EXECUTABLE)
	message(FATAL_ERROR "glslc not found. This is needed to compile shaders. Please install shaderc or load the Vulkan SDK.")
else()
	message("-- Found glslc: ${Vulkan_GLSLC_EXECUTABLE}")
endif()

# This is needed due to VkFFT not using a standard path for glslang_c_interface.h
get_target_property(glslang_INCLUDE_DIR glslang::glslang INTERFACE_INCLUDE_DIRECTORIES)
# Find MoltenVK on macOS, no need to link it later
if(APPLE)
	find_library(MOLTENVK_LIBRARIES NAMES MoltenVK libMoltenVK)
	if( NOT MOLTENVK_LIBRARIES )
		message( FATAL_ERROR "scopehal-apps requires MoltenVK to be installed in order to function on macOS.")
	endif()
endif()

if(NOT WIN32)
	include(GNUInstallDirs)
endif()

# Documentation
add_subdirectory("${PROJECT_SOURCE_DIR}/doc")
if(NOT BUILD_DOCS)
	set_property(TARGET doc PROPERTY EXCLUDE_FROM_ALL ON)
endif()

# Static analysis
if(ANALYZE)
	find_program(CPPCHECK_PATH cppcheck DOC "Path to cppcheck when ANALYZE is enabled")
	if(CPPCHECK_PATH)
		execute_process(COMMAND ${CPPCHECK_PATH} "--version" OUTPUT_VARIABLE CPPCHECK_VER_STR ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
		string(REPLACE "Cppcheck " "" CPPCHECK_VERSION ${CPPCHECK_VER_STR})
		if(CPPCHECK_VERSION VERSION_GREATER_EQUAL "2")
			set(CMAKE_CXX_CPPCHECK "${CPPCHECK_PATH};-DFT_USE_AUTOCONF_SIZEOF_TYPES;-D__GNUC__;--enable=warning,performance,portability;--suppress=*:*sigc*;--suppress=*:*glibmm*;--suppress=*:*gtkmm*;--inline-suppr;-q;--std=c++11")
			message(STATUS "Found CPPCheck: ${CPPCHECK_PATH} (found version \"${CPPCHECK_VERSION}\")")
		else()
			message(STATUS "Found CPPCheck: ${CPPCHECK_PATH} but ignored it as it was ${CPPCHECK_VERSION} < 2")
		endif()
	else()
		message(FATAL_ERROR "CPPCheck not found, required for ANALYZE")
	endif()
	# The actual clang-analyzer compiler wrapper doesn't get installed on $PATH, only scan-build which is useless to us
	find_program(CLANGANALYZER_SCANBUILD_PATH scan-build DOC "Path to clang-analyzer's scan-build tool, used as a hint to find the rest of the clang-analyzer")
	get_filename_component(CLANGANALYZER_SCANBUILD_BIN ${CLANGANALYZER_SCANBUILD_PATH} REALPATH)
	get_filename_component(CLANGANALYZER_BIN_PATH ${CLANGANALYZER_SCANBUILD_BIN} DIRECTORY)
	find_program(CLANGANALYZER_CXXANALYZER_PATH "c++-analyzer" HINTS "${CLANGANALYZER_BIN_PATH}/../libexec" DOC "Path to clang-analyzer's c++-analyzer")
	if(CLANGANALYZER_CXXANALYZER_PATH)
		set(CMAKE_CXX_COMPILER_LAUNCHER "${CLANGANALYZER_CXXANALYZER_PATH}")
		message(STATUS "Found clang-analyzer: ${CLANGANALYZER_CXXANALYZER_PATH}")
	else()
		message(FATAL_ERROR "clang-analyzer not found, required for ANALYZE")
	endif()
endif()

# Main project code
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/scopehal")
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/scopeprotocols")
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/xptools")
add_subdirectory("${PROJECT_SOURCE_DIR}/lib/log")
add_subdirectory("${PROJECT_SOURCE_DIR}/src/ngscopeclient")

add_subdirectory("${PROJECT_SOURCE_DIR}/src/nativefiledialog-extended")

add_subdirectory(devdoc)

# Unit tests
if(Git_FOUND AND BUILD_TESTING)
	find_package(Catch2 REQUIRED)
	include(Catch)
	#Catch2 v3.x.y have a breaking change:
	# Must include catch2/catch_all.hpp instead of catch2/catch.hpp
	# So we set a compile flag to let the code know.
	if(NOT Catch2_VERSION MATCHES "^[0-2]\\.")
		add_compile_options(-D_CATCH2_V3)
	endif()
	add_subdirectory("${PROJECT_SOURCE_DIR}/tests")
endif()

# Example code and other utilities, don't build on non-POSIX yet
if(NOT WIN32)
	add_subdirectory("${PROJECT_SOURCE_DIR}/src/examples/curvetrace")
	#add_subdirectory("${PROJECT_SOURCE_DIR}/src/examples/usbcsv")
endif()

# Make sure all of our shared libraries are built relocatable
set_property(TARGET scopehal PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET log PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET xptools PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET scopeprotocols PROPERTY POSITION_INDEPENDENT_CODE ON)

# CPack package generation
set(CPACK_PACKAGE_NAME "ngscopeclient")
set(CPACK_PACKAGE_VENDOR "ngscopeclient.org project")
set(CPACK_PACKAGE_VERSION_PATCH "0+${NGSCOPECLIENT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION "Cross platform T&M remote control and signal analysis suite")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.ngscopeclient.org/")
set(CPACK_THREADS 0)
set(CPACK_STRIP_FILES TRUE)

# Figure out what distro version we're on
if(LINUX)
	find_program(LSB_RELEASE_EXEC lsb_release REQUIRED)
	execute_process(COMMAND ${LSB_RELEASE_EXEC} -is
		OUTPUT_VARIABLE DISTRO_NAME
		OUTPUT_STRIP_TRAILING_WHITESPACE
		COMMAND_ERROR_IS_FATAL ANY)
	execute_process(COMMAND ${LSB_RELEASE_EXEC} -rs
		OUTPUT_VARIABLE DISTRO_VER
		OUTPUT_STRIP_TRAILING_WHITESPACE
		COMMAND_ERROR_IS_FATAL ANY)

	message(STATUS "Linux distribution target for packaging: name ${DISTRO_NAME}, version ${DISTRO_VER}")

	# Debian specific packaging config
	set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Andrew Zonenberg <andrew.zonenberg@antikernel.net>")
	if(DISTRO_NAME STREQUAL "Debian")

		# Bookworm
		# Versions are based on what Bookworm is shipping as of 2024-10-30
		if(DISTRO_VER STREQUAL "12")
			set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.36-9), libpng16-16(>= 1.6.39-2), libsigc++-2.0-0v5 (>= 2.12.0-1), libyaml-cpp0.7(>= 0.7.0), libgomp1(>= 12.2.0), libvulkan1(>= 1.3.239), libglfw3(>= 3.3.8), libgtk-3-0(>= 3.24.38), zlib1g(>= 1.2.13), libhidapi-hidraw0(>= 0.13.1), liblxi1(>= 1.18), libtirpc3 (>= 1.3.3)" )

		# Trixie
		# Versions are based on what Trixie is shipping as of 2025-09-20
		elseif(DISTRO_VER STREQUAL "13")
			set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.41-12), libpng16-16t64(>= 1.6.48), libsigc++-2.0-0v5 (>= 2.12.1), libyaml-cpp0.8(>= 0.8.0), libgomp1(>= 14.2.0-19), libvulkan1(>= 1.4.309.0), libglfw3(>= 3.4-3), libgtk-3-0t64(>= 3.24.49), zlib1g(>= 1:1.3.dfsg+really1.3.1-1), libhidapi-hidraw0(>= 0.14.0-1), liblxi1(>= 1.22-1), libtirpc3t64 (>= 1.3.6+ds-1)" )
		endif()

	# Ubuntu specific packaging config
	elseif(DISTRO_NAME STREQUAL "Ubuntu")

		if(DISTRO_VER STREQUAL "22.04")
			set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.35-0), libpng16-16(>= 1.6.37-3), libsigc++-2.0-0v5 (>= 2.10.4-2), libyaml-cpp0.7(>= 0.7.0), libgomp1(>= 12.2.0), libvulkan1(>= 1.3.204.1-2), libglfw3(>= 3.3.6-1), libgtk-3-0(>= 3.24.33-1), zlib1g(>= 1:1.2.11), libhidapi-hidraw0(>= 0.11.2-1), liblxi1(>= 1.16-1), libtirpc3 (>= 1.3.2-2)" )
		elseif(DISTRO_VER STREQUAL "24.04")
			#set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.36-9), libpng16-16(>= 1.6.39-2), libsigc++-2.0-0v5 (>= 2.12.0-1), libyaml-cpp0.7(>= 0.7.0), libgomp1(>= 12.2.0), libvulkan1(>= 1.3.239), libglfw3(>= 3.3.8), libgtk-3-0(>= 3.24.38), zlib1g(>= 1.2.13), libhidapi-hidraw0(>= 0.13.1), liblxi1(>= 1.18), libtirpc3 (>= 1.3.3)" )
		endif()

	# Unrecognized Debian-derived distro settings (assume Bookworm for now)
	else()
		set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.36-9), libpng16-16(>= 1.6.39-2), libsigc++-2.0-0v5 (>= 2.12.0-1), libyaml-cpp0.7(>= 0.7.0), libgomp1(>= 12.2.0), libvulkan1(>= 1.3.239), libglfw3(>= 3.3.8), libgtk-3-0(>= 3.24.38), zlib1g(>= 1.2.13), libhidapi-hidraw0(>= 0.13.1), liblxi1(>= 1.18), libtirpc3 (>= 1.3.3)" )
	endif()

endif()

if(APPLE)
    set(CPACK_BUNDLE "ON")
	set(CPACK_BUNDLE_PLIST "${PROJECT_SOURCE_DIR}/src/ngscopeclient/macos/Info.plist")
	# Must be generated with:
	# iconutil -c icns ngscopeclient.iconset
	set(CPACK_BUNDLE_ICON "${PROJECT_SOURCE_DIR}/src/ngscopeclient/icons/macos/ngscopeclient.icns")
	set(CPACK_PACKAGE_ICON "${PROJECT_SOURCE_DIR}/src/ngscopeclient/icons/macos/ngscopeclient.icns")
    set(CPACK_BUNDLE_NAME "ngscopeclient")
	set(CPACK_BUNDLE_STARTUP_COMMAND "${PROJECT_SOURCE_DIR}/src/ngscopeclient/macos/ngscopeclient.sh")
	set_target_properties(ngscopeclient PROPERTIES
		INSTALL_RPATH "@executable_path/../lib;/usr/local/lib;/opt/homebrew/lib")
endif()

# this must be at the very end *after* we've done all the config
include(CPack)
